
package appserver;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import rpc.RPCClient;
import rpc.RPCServer;
import session.SSMCookie;
import session.Session;
import session.SessionID;

/**
 * displayed evictions will not include overwrites on updates
 */

@SuppressWarnings("serial")
@WebServlet("/main")
public class MyLBServlet extends HttpServlet {
	private ConcurrentHashMap<SessionID, Session> sessionMap;	
	private Set<String> mbrSet;
	private Cache cache; 
	private AtomicInteger localSessionNo = new AtomicInteger(1); //atomically increment local session numbers
	private RPCServer rpcServer;
	private Integer rpcPort= ServletUtils.getRandomNumber(32000, 64000); 
	private static final String COOKIE_NAME = Constants.CS_5300_COOKIE_NAME; 
	private static final boolean DEBUG = true; // a "verbose" flag
	private static final String GREETING = Constants.NEW_USR_GRTNG; 

	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		sessionMap = new ConcurrentHashMap<SessionID, Session>();
		cache= new Cache(Constants.CACHE_SIZE);
		mbrSet= new ConcurrentSkipListSet<String>(); 
		rpcServer= new RPCServer(rpcPort, sessionMap, mbrSet);
		new Thread(rpcServer).start();
		SessionCleaner cleaningThread = new SessionCleaner(sessionMap);
		cleaningThread.start();
	}
	
	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response) 
			  throws ServletException, IOException {
		
		if (request.getParameter("cmd") != null && request.getParameter("cmd").equals("CrashServer")) 
		{
			System.exit(0);
		}
		
		PrintWriter out = response.getWriter();
		String servIP= InetAddress.getLocalHost().getHostAddress();
		Integer servPort = rpcPort;
		String servIPP= servIP + Constants.PORT_NUMBER_SEPARATOR + servPort;
		
		
		if (!mbrSet.contains(servIPP)) {
			mbrSet.add(servIPP);
			
		}
		//three booleans used to describe input case
		Boolean dataHere= false;
		Boolean newVis= false;
		Boolean buNull= false; //backup null
		
		//session info
		SessionID sid = null;
		Session curSess= null;
		SSMCookie curCook= null;   //contains both SID and SVN 
		Session evicted= null; //used to track cache evictions.  Will only be one element, either overflow or stale
		
		Cookie cookie = ServletUtils.getCookie(COOKIE_NAME, request.getCookies());
		if (cookie == null || (cookie != null && cookie.getValue() == null)) 
		{
			sid= new SessionID(servIPP, localSessionNo.getAndIncrement());
			String newCookVal= SSMCookie.buildStandardValue(sid.toString(), 1, servIPP, Constants.IP_NULL);  //must be set later, uses same methods as an update
			curCook= new SSMCookie(COOKIE_NAME, newCookVal);
			curSess= new Session(sid, Constants.TIMEOUT_UPDATE_SECS, GREETING);
			dataHere=true;
			newVis=true;		
		}

		else {
			curCook= new SSMCookie(cookie); 
			sid = SessionID.parseString(curCook.getSessionId());
		}
		
		buNull = (curCook.getIPBackup().equals(Constants.IP_NULL));
		
	//**Getting session information
		//(new cooks when new sessions set both IPPS to Null, thus skipped)
		String primary = curCook.getIPPrimary();
		String backup = curCook.getIPBackup();
		if((primary != null && primary.equals(servIPP)) || (backup != null && backup.equals(servIPP)))
		{
			if (sessionMap.containsKey(sid))
			{
				Session trySess= sessionMap.get(sid);
				if (trySess != null && trySess.getSessionVersion()==curCook.getSessionVersion())
				{
					curSess= trySess;
					dataHere=  true;
				}
			}
		}
		//**2nd case, check cache
		if(!dataHere && !newVis && cache.containsKey(sid) ) {
			if (cache.hasFreshVersion(sid, curCook)) {
				curSess= cache.get(sid);
				dataHere= true;
			}
			else {
				evicted= cache.flushStale(sid);
			}
		}
		
		
		if (!dataHere && !newVis)
		{
			RPCClient rpcClient= new RPCClient(mbrSet, servIPP, rpcPort);
			String[] rpcServs= {curCook.getIPPrimary(), curCook.getIPBackup()};
			curSess= rpcClient.sessionRead(sid.toString(), curCook.getSessionVersion(), rpcServs);
			//update cache with RPC response
			if (curSess != null){
				Session excess= cache.put(sid, curSess);
				if (excess!= null)  //will be null if stale check, because room, so record of evicted holds
					evicted= excess;
			}
		}
	
		if (curSess==null){
			badSessReturn(sid, request, response);
			return;
		}
		
		if (request.getParameter("cmd") != null && request.getParameter("cmd").equals("LogOut")) {
			response.setContentType("text/html");
			out.println
			("<!DOCTYPE html>\n" +
					"<html>\n" +
					"<head><title> load Balanced session management </title></head>\n" +
					"<body bgcolor=\"#fdf5e6\">\n" +
					"<h1 align= left> Farewell, we are boxing up your belongings</h1>\n" +
					"</body></html>");
			Cookie loCookie= new Cookie(COOKIE_NAME, "logout");
			loCookie.setMaxAge(0);
			response.addCookie(loCookie);
			//deletes session from server
			RPCClient rpcLO= new RPCClient(mbrSet, servIPP, rpcPort);
			rpcLO.delete(sid, curCook.getIPPrimary());
			rpcLO.delete(sid, curCook.getIPBackup());
			sessionMap.remove(sid);
			return;
		}	
		
	    //fix the null case
		if (buNull && !curCook.getIPPrimary().equals(servIPP)) //skips new cookies
		{
			sessionMap.put(sid, curSess);
			curCook.setIPBackup(servIPP);  
			buNull=false;
		}
		
		if ((!newVis && (request.getParameter("cmd") == null || request.getParameter("cmd").equals("Refresh")))) {
			boolean containedBefore = false;
			synchronized (curSess) {
				if (curSess.needsUpdate())
				curSess.setDiscard_time(Constants.TIMEOUT_UPDATE_SECS);
				containedBefore = sessionMap.containsKey(curSess.getSessionID());
			}
			if (containedBefore && !sessionMap.containsKey(curSess.getSessionID())) {
				badSessReturn(curSess.getSessionID(), request, response);
				return;
			}
			
			
		}
		
		else if (!newVis && request.getParameter("cmd").equals("Replace")) {
			curSess.updateVersion();
			curSess.setMessage(request.getParameter("NewText") == null ? "" : request.getParameter("NewText"));
			boolean containedBefore = false;
			synchronized (curSess) {
				if (curSess.needsUpdate())
				curSess.setDiscard_time(Constants.TIMEOUT_UPDATE_SECS);
				containedBefore = sessionMap.containsKey(curSess.getSessionID());
			}
			if (containedBefore && !sessionMap.containsKey(curSess.getSessionID())) {
				badSessReturn(curSess.getSessionID(), request, response);
				return;
			}
			
			RPCClient rpcW= new RPCClient(mbrSet, servIPP, rpcPort);
			String backupIpp = rpcW.sessionWrite(curSess.getSessionID(), curSess.getSessionVersion(), curSess, curSess.getDiscard_time());
			sessionMap.put(curSess.getSessionID(), curSess);
			
			//explicit garbage collection
			if ((!curCook.getIPPrimary().equals(servIPP)) && (!curCook.getIPPrimary().equals(backupIpp) ))
			{
				rpcW.delete(sid, curCook.getIPPrimary()); 
			}
			
			if ((!curCook.getIPBackup().equals(servIPP)) && (!curCook.getIPBackup().equals(backupIpp)))
			{
				rpcW.delete(sid, curCook.getIPBackup()); 
			}
			
			curCook.setIPPrimary(servIPP);
			if (backupIpp == null) {
				curCook.setIPBackup(Constants.IP_NULL);
			}
			else {
				curCook.setIPBackup(backupIpp);
			}			
		}
		
		else if (newVis)
		{
			RPCClient rpcW= new RPCClient(mbrSet, servIPP, rpcPort);
			String backupIpp = rpcW.sessionWrite(sid, curSess.getSessionVersion(), curSess, Constants.TIMEOUT_UPDATE_SECS);
			curCook.setIPPrimary(servIPP);
			if (backupIpp == null) {
				curCook.setIPBackup(Constants.IP_NULL);
			}
			else {
				curCook.setIPBackup(backupIpp);
			}
			sessionMap.put(sid, curSess);
		}
		response.setContentType("text/html");
		//generate html (probably better passed to a JSP, but it's easy enough for such a small page)
		StringBuffer responseSB = new StringBuffer();
		responseSB.append(ServletUtils.headWithTitle("Welcome"));
		responseSB.append("<body>");
		responseSB.append("<p> " + curSess.getMessage() + "</p>");
		responseSB.append(ServletUtils.generateForm("GET", "cmd", "Replace", "submit", true));
		responseSB.append(ServletUtils.generateForm("GET", "cmd", "Refresh", "submit", false));
		responseSB.append(ServletUtils.generateForm("GET", "cmd", "LogOut", "submit", false));
		responseSB.append("<p>" + "Your session is good until at least: " + ServletUtils.formatDate(curSess.getDiscard_time()) + "</p>");
		//extra ui pieces for grade/debugging
		responseSB.append(ServletUtils.generateForm("GET", "cmd", "CrashServer", "submit", false));
		if (DEBUG) responseSB.append("<p> this server IPP: " + servIPP + "<p>");
		if (DEBUG) responseSB.append("<p> was data in this server without rpc call? : " + dataHere + "<p>");
		if (DEBUG) responseSB.append("<p> IPP-primary: " + curCook.getIPPrimary() + " IPP-backup: " + curCook.getIPBackup() + " session-expiration: " + curSess.getDiscard_time() + " session-timeout : still must implement timeout" +"<p>");

		
		//shows session and version website for debugging purposes- vestigal from original	
		if (DEBUG) {
			responseSB.append("<p> Version: " + curSess.getSessionVersion() + "</p>");
			responseSB.append("<p> SessionID: " + curSess.getSessionID() + "</p>");
		}
	
		out.write(responseSB.toString());
		if (evicted != null)
			out.write("<p> evicted from cache: " + evicted.getSessionID() + " version:" + evicted.getSessionVersion() +  "</p> ");
		
		out.write("<p> servers in mbr set: ");
		int servCount= 1;
		for(String serv: mbrSet)
				 {
					out.write(servCount + ": " + serv);
					servCount++;
				 }
		out.write("</body></html>");
		
		Cookie respCook = new Cookie(Constants.CS_5300_COOKIE_NAME, SSMCookie.buildStandardValue(curSess.getSessionID().toString(), curSess.getSessionVersion(), curCook.getIPPrimary(), curCook.getIPBackup()));
		response.addCookie(respCook);
	}
	
	
	private void badSessReturn(SessionID sessId, HttpServletRequest request, HttpServletResponse response)
	{
		PrintWriter badout;
		try {
			response.setContentType("text/html");
			badout = response.getWriter();
			badout.println
			("<!DOCTYPE html>\n" +
					"<html>\n" +
					"<head><title> load Balanced session management  </title></head>\n" +
					"<body bgcolor=\"#fdf5e6\">\n" +
					"<h1 align= left> Timed out or bad cookie, please refresh and get treated as though we never knew you</h1>\n" +
					"</body></html>");	
			Cookie loCookie= new Cookie(COOKIE_NAME, null);
			loCookie.setMaxAge(0);
			response.addCookie(loCookie);	
		}catch (IOException e) {
		
		e.printStackTrace();
		}
	}

}
